import { ContextLimits } from '../../Source/Cesium.js';
import { ShaderProgram } from '../../Source/Cesium.js';
import { ShaderSource } from '../../Source/Cesium.js';
import createContext from '../createContext.js';

describe('Renderer/ShaderProgram', function() {

    var webglStub = !!window.webglStub;
    var context;
    var sp;

    var injectedTestFunctions = {
        czm_circularDependency1 : 'void czm_circularDependency1() { czm_circularDependency2(); }',
        czm_circularDependency2 : 'void czm_circularDependency2() { czm_circularDependency1(); }',
        czm_testFunction3 : 'void czm_testFunction3(vec4 color) { czm_testFunction2(color); }',
        czm_testFunction2 : 'void czm_testFunction2(vec4 color) { czm_testFunction1(color); }',
        czm_testFunction1 : 'void czm_testFunction1(vec4 color) { gl_FragColor = color; }',
        czm_testDiamondDependency1 : 'vec4 czm_testDiamondDependency1(vec4 color) { return czm_testAddAlpha(color); }',
        czm_testDiamondDependency2 : 'vec4 czm_testDiamondDependency2(vec4 color) { return czm_testAddAlpha(color); }',
        czm_testAddAlpha : 'vec4 czm_testAddAlpha(vec4 color) { color.a = clamp(color.a + 0.1, 0.0, 1.0); return color; }',
        czm_testAddRed : 'vec4 czm_testAddRed(vec4 color) { color.r = clamp(color.r + 0.1, 0.0, 1.0); return color; }',
        czm_testAddGreen : 'vec4 czm_testAddGreen(vec4 color) { color.g = clamp(color.g + 0.1, 0.0, 1.0); return color; }',
        czm_testAddRedGreenAlpha : 'vec4 czm_testAddRedGreenAlpha(vec4 color) { color = czm_testAddRed(color); color = czm_testAddGreen(color); color = czm_testAddAlpha(color); return color; }',
        czm_testFunction4 : 'void czm_testFunction4(vec4 color) { color = czm_testAddAlpha(color); color = czm_testAddRedGreenAlpha(color); czm_testFunction3(color); }',
        czm_testFunctionWithComment : '/**\n czm_circularDependency1()  \n*/\nvoid czm_testFunctionWithComment(vec4 color) { czm_testFunction1(color); }'
    };

    beforeAll(function() {
        context = createContext();

        for ( var functionName in injectedTestFunctions) {
            if (injectedTestFunctions.hasOwnProperty(functionName)) {
                ShaderSource._czmBuiltinsAndUniforms[functionName] = injectedTestFunctions[functionName];
            }
        }
    });

    afterAll(function() {
        context.destroyForSpecs();

        for ( var functionName in injectedTestFunctions) {
            if (injectedTestFunctions.hasOwnProperty(functionName)) {
                delete ShaderSource._czmBuiltinsAndUniforms[functionName];
            }
        }
    });

    afterEach(function() {
        sp = sp && sp.destroy();
    });

    it('has vertex and fragment shader source', function() {
        var vs = 'void main() { gl_Position = vec4(1.0); }';
        var fs = 'void main() { gl_FragColor = vec4(1.0); }';
        sp = ShaderProgram.fromCache({
            context : context,
            vertexShaderSource : vs,
            fragmentShaderSource : fs
        });

        var expectedVSText = new ShaderSource({
            sources : [vs]
        }).createCombinedVertexShader(context);

        expect(sp._vertexShaderText).toEqual(expectedVSText);

        var expectedFSText = new ShaderSource({
            sources : [fs]
        }).createCombinedFragmentShader(context);

        expect(sp._fragmentShaderText).toEqual(expectedFSText);
    });

    it('has a position vertex attribute', function() {
        var vs = 'attribute vec4 position; void main() { gl_Position = position; }';
        var fs = 'void main() { gl_FragColor = vec4(1.0); }';
        sp = ShaderProgram.fromCache({
            context : context,
            vertexShaderSource : vs,
            fragmentShaderSource : fs
        });

        if (webglStub) {
            return; // WebGL Stub does not return vertex attribute and uniforms in the shader
        }

        expect(sp.numberOfVertexAttributes).toEqual(1);
        expect(sp.vertexAttributes.position.name).toEqual('position');
    });

    it('sets attribute indices', function() {
        var vs =
            'attribute vec4 position;' +
            'attribute vec3 normal;' +
            'attribute float heat;' +
            'void main() { gl_Position = position + vec4(normal, 0.0) + vec4(heat); }';
        var fs = 'void main() { gl_FragColor = vec4(1.0); }';

        var attributes = {
            position : 3,
            normal : 2,
            heat : 1
        };

        sp = ShaderProgram.fromCache({
            context : context,
            vertexShaderSource : vs,
            fragmentShaderSource : fs,
            attributeLocations : attributes
        });

        if (webglStub) {
            return; // WebGL Stub does not return vertex attribute and uniforms in the shader
        }

        expect(sp.numberOfVertexAttributes).toEqual(3);
        expect(sp.vertexAttributes.position.name).toEqual('position');
        expect(sp.vertexAttributes.position.index).toEqual(attributes.position);
        expect(sp.vertexAttributes.normal.name).toEqual('normal');
        expect(sp.vertexAttributes.normal.index).toEqual(attributes.normal);
        expect(sp.vertexAttributes.heat.name).toEqual('heat');
        expect(sp.vertexAttributes.heat.index).toEqual(attributes.heat);
    });

    it('has an automatic uniform', function() {
        var vs = 'uniform vec4 u_vec4; void main() { gl_Position = u_vec4; }';
        var fs = 'void main() { gl_FragColor = vec4((czm_viewport.x == 0.0) && (czm_viewport.y == 0.0) && (czm_viewport.z == 1.0) && (czm_viewport.w == 1.0)); }';
        sp = ShaderProgram.fromCache({
            context : context,
            vertexShaderSource : vs,
            fragmentShaderSource : fs
        });

        if (webglStub) {
            return; // WebGL Stub does not return vertex attribute and uniforms in the shader
        }

        expect(sp.allUniforms.u_vec4.name).toEqual('u_vec4');
        expect(sp.allUniforms.czm_viewport.name).toEqual('czm_viewport');
    });

    it('has uniforms of every datatype', function() {
        var d = context;
        var vs =
            'uniform float u_float;' +
            'uniform vec2 u_vec2;' +
            'uniform vec3 u_vec3;' +
            'uniform vec4 u_vec4;' +
            'uniform int u_int;' +
            'uniform ivec2 u_ivec2;' +
            'uniform ivec3 u_ivec3;' +
            'uniform ivec4 u_ivec4;' +
            'uniform bool u_bool;' +
            'uniform bvec2 u_bvec2;' +
            'uniform bvec3 u_bvec3;' +
            'uniform bvec4 u_bvec4;' +
            'uniform mat2 u_mat2;' +
            'uniform mat3 u_mat3;' +
            'uniform mat4 u_mat4;' +
            'void main() { gl_Position = vec4(u_float) * vec4((u_mat2 * u_vec2), 0.0, 0.0) * vec4((u_mat3 * u_vec3), 0.0) * (u_mat4 * u_vec4) * vec4(u_int) * vec4(u_ivec2, 0.0, 0.0) * vec4(u_ivec3, 0.0) * vec4(u_ivec4) * vec4(u_bool) * vec4(u_bvec2, 0.0, 0.0) * vec4(u_bvec3, 0.0) * vec4(u_bvec4); }';
        var fs =
            'uniform sampler2D u_sampler2D;' +
            'uniform samplerCube u_samplerCube;' +
            'void main() { gl_FragColor = texture2D(u_sampler2D, vec2(0.0)) + textureCube(u_samplerCube, vec3(1.0)); }';
        sp = ShaderProgram.fromCache({
            context : d,
            vertexShaderSource : vs,
            fragmentShaderSource : fs
        });

        if (webglStub) {
            return; // WebGL Stub does not return vertex attribute and uniforms in the shader
        }

        expect(sp.allUniforms.u_float.name).toEqual('u_float');
        expect(sp.allUniforms.u_vec2.name).toEqual('u_vec2');
        expect(sp.allUniforms.u_vec3.name).toEqual('u_vec3');
        expect(sp.allUniforms.u_vec4.name).toEqual('u_vec4');
        expect(sp.allUniforms.u_int.name).toEqual('u_int');
        expect(sp.allUniforms.u_ivec2.name).toEqual('u_ivec2');
        expect(sp.allUniforms.u_ivec3.name).toEqual('u_ivec3');
        expect(sp.allUniforms.u_ivec4.name).toEqual('u_ivec4');
        expect(sp.allUniforms.u_bool.name).toEqual('u_bool');
        expect(sp.allUniforms.u_bvec2.name).toEqual('u_bvec2');
        expect(sp.allUniforms.u_bvec3.name).toEqual('u_bvec3');
        expect(sp.allUniforms.u_bvec4.name).toEqual('u_bvec4');
        expect(sp.allUniforms.u_mat2.name).toEqual('u_mat2');
        expect(sp.allUniforms.u_mat3.name).toEqual('u_mat3');
        expect(sp.allUniforms.u_mat4.name).toEqual('u_mat4');
        expect(sp.allUniforms.u_sampler2D.name).toEqual('u_sampler2D');
        expect(sp.allUniforms.u_samplerCube.name).toEqual('u_samplerCube');
    });

    it('has a struct uniform', function() {
        var vs = 'uniform struct { float f; vec4 v; } u_struct; void main() { gl_Position = u_struct.f * u_struct.v; }';
        var fs = 'void main() { gl_FragColor = vec4(1.0); }';
        sp = ShaderProgram.fromCache({
            context : context,
            vertexShaderSource : vs,
            fragmentShaderSource : fs
        });

        if (webglStub) {
            return; // WebGL Stub does not return vertex attribute and uniforms in the shader
        }

        expect(sp.allUniforms['u_struct.f'].name).toEqual('u_struct.f');
        expect(sp.allUniforms['u_struct.v'].name).toEqual('u_struct.v');
    });

    it('has uniform arrays of every datatype', function() {
        var d = context;
        var vs =
            'uniform float u_float[2];' +
            'uniform vec2 u_vec2[2];' +
            'uniform vec3 u_vec3[2];' +
            'uniform vec4 u_vec4[2];' +
            'uniform int u_int[2];' +
            'uniform ivec2 u_ivec2[2];' +
            'uniform ivec3 u_ivec3[2];' +
            'uniform ivec4 u_ivec4[2];' +
            'uniform bool u_bool[2];' +
            'uniform bvec2 u_bvec2[2];' +
            'uniform bvec3 u_bvec3[2];' +
            'uniform bvec4 u_bvec4[2];' +
            'uniform mat2 u_mat2[2];' +
            'uniform mat3 u_mat3[2];' +
            'uniform mat4 u_mat4[2];' +
            'void main() { gl_Position = vec4(u_float[0]) * vec4(u_float[1]) * vec4((u_mat2[0] * u_vec2[0]), 0.0, 0.0) * vec4((u_mat2[1] * u_vec2[1]), 0.0, 0.0) * vec4((u_mat3[0] * u_vec3[0]), 0.0) * vec4((u_mat3[1] * u_vec3[1]), 0.0) * (u_mat4[0] * u_vec4[0]) * (u_mat4[1] * u_vec4[1]) * vec4(u_int[0]) * vec4(u_int[1]) * vec4(u_ivec2[0], 0.0, 0.0) * vec4(u_ivec2[1], 0.0, 0.0) * vec4(u_ivec3[0], 0.0) * vec4(u_ivec3[1], 0.0) * vec4(u_ivec4[0]) * vec4(u_ivec4[1]) * vec4(u_bool[0]) * vec4(u_bool[1]) * vec4(u_bvec2[0], 0.0, 0.0) * vec4(u_bvec2[1], 0.0, 0.0) * vec4(u_bvec3[0], 0.0) * vec4(u_bvec3[1], 0.0) * vec4(u_bvec4[0]) * vec4(u_bvec4[1]); }';
        var fs =
            'uniform sampler2D u_sampler2D[2];' +
            'uniform samplerCube u_samplerCube[2];' +
            'void main() { gl_FragColor = texture2D(u_sampler2D[0], vec2(0.0)) + texture2D(u_sampler2D[1], vec2(0.0)) + textureCube(u_samplerCube[0], vec3(1.0)) + textureCube(u_samplerCube[1], vec3(1.0)); }';
        sp = ShaderProgram.fromCache({
            context : d,
            vertexShaderSource : vs,
            fragmentShaderSource : fs
        });

        if (webglStub) {
            return; // WebGL Stub does not return vertex attribute and uniforms in the shader
        }

        expect(sp.allUniforms.u_float.name).toEqual('u_float');
        expect(sp.allUniforms.u_vec2.name).toEqual('u_vec2');
        expect(sp.allUniforms.u_vec3.name).toEqual('u_vec3');
        expect(sp.allUniforms.u_vec4.name).toEqual('u_vec4');
        expect(sp.allUniforms.u_int.name).toEqual('u_int');
        expect(sp.allUniforms.u_ivec2.name).toEqual('u_ivec2');
        expect(sp.allUniforms.u_ivec3.name).toEqual('u_ivec3');
        expect(sp.allUniforms.u_ivec4.name).toEqual('u_ivec4');
        expect(sp.allUniforms.u_bool.name).toEqual('u_bool');
        expect(sp.allUniforms.u_bvec2.name).toEqual('u_bvec2');
        expect(sp.allUniforms.u_bvec3.name).toEqual('u_bvec3');
        expect(sp.allUniforms.u_bvec4.name).toEqual('u_bvec4');
        expect(sp.allUniforms.u_mat2.name).toEqual('u_mat2');
        expect(sp.allUniforms.u_mat3.name).toEqual('u_mat3');
        expect(sp.allUniforms.u_mat4.name).toEqual('u_mat4');
        expect(sp.allUniforms.u_sampler2D.name).toEqual('u_sampler2D');
        expect(sp.allUniforms.u_samplerCube.name).toEqual('u_samplerCube');

        expect(sp.allUniforms.u_float.value.length).toEqual(2);
        expect(sp.allUniforms.u_vec2.value.length).toEqual(2);
        expect(sp.allUniforms.u_vec3.value.length).toEqual(2);
        expect(sp.allUniforms.u_vec4.value.length).toEqual(2);
        expect(sp.allUniforms.u_int.value.length).toEqual(2);
        expect(sp.allUniforms.u_ivec2.value.length).toEqual(2);
        expect(sp.allUniforms.u_ivec3.value.length).toEqual(2);
        expect(sp.allUniforms.u_ivec4.value.length).toEqual(2);
        expect(sp.allUniforms.u_bool.value.length).toEqual(2);
        expect(sp.allUniforms.u_bvec2.value.length).toEqual(2);
        expect(sp.allUniforms.u_bvec3.value.length).toEqual(2);
        expect(sp.allUniforms.u_bvec4.value.length).toEqual(2);
        expect(sp.allUniforms.u_mat2.value.length).toEqual(2);
        expect(sp.allUniforms.u_mat3.value.length).toEqual(2);
        expect(sp.allUniforms.u_mat4.value.length).toEqual(2);
        expect(sp.allUniforms.u_sampler2D.value.length).toEqual(2);
        expect(sp.allUniforms.u_samplerCube.value.length).toEqual(2);
    });

    it('has predefined constants', function() {
        var fs =
            'void main() { ' +
            '  float f = ((czm_pi > 0.0) && \n' +
            '    (czm_oneOverPi > 0.0) && \n' +
            '    (czm_piOverTwo > 0.0) && \n' +
            '    (czm_piOverThree > 0.0) && \n' +
            '    (czm_piOverFour > 0.0) && \n' +
            '    (czm_piOverSix > 0.0) && \n' +
            '    (czm_threePiOver2 > 0.0) && \n' +
            '    (czm_twoPi > 0.0) && \n' +
            '    (czm_oneOverTwoPi > 0.0) && \n' +
            '    (czm_radiansPerDegree > 0.0) && \n' +
            '    (czm_degreesPerRadian > 0.0)) ? 1.0 : 0.0; \n' +
            '  gl_FragColor = vec4(f); \n' +
            '}';

        expect({
            context : context,
            fragmentShader : fs
        }).contextToRender();
    });

    it('has built-in constant, structs, and functions', function() {
        var fs =
            'void main() { \n' +
            '  czm_materialInput materialInput; \n' +
            '  czm_material material = czm_getDefaultMaterial(materialInput); \n' +
            '  material.diffuse = vec3(1.0, 1.0, 1.0); \n' +
            '  material.alpha = 1.0; \n' +
            '  material.diffuse = czm_hue(material.diffuse, czm_twoPi); \n' +
            '  gl_FragColor = vec4(material.diffuse, material.alpha); \n' +
            '}';

        expect({
            context : context,
            fragmentShader : fs
        }).contextToRender();
    });

    it('creates duplicate uniforms if precision of uniforms in vertex and fragment shader do not match', function() {
        var highpFloatSupported = ContextLimits.highpFloatSupported;
        ContextLimits._highpFloatSupported = false;
        var vs = 'attribute vec4 position; uniform float u_value; varying float v_value; void main() { gl_PointSize = 1.0; v_value = u_value * czm_viewport.z; gl_Position = position; }';
        var fs = 'uniform float u_value; varying float v_value; void main() { gl_FragColor = vec4(u_value * v_value * czm_viewport.z); }';
        var uniformMap = {
            u_value : function() {
                return 1.0;
            }
        };

        sp = ShaderProgram.fromCache({
            context : context,
            vertexShaderSource : vs,
            fragmentShaderSource : fs
        });

        if (!webglStub) {
            // WebGL Stub does not return vertex attribute and uniforms in the shader
            expect(sp.allUniforms.u_value).toBeDefined();
            expect(sp.allUniforms.czm_mediump_u_value).toBeDefined();
        }

        expect({
            context : context,
            vertexShader : vs,
            fragmentShader : fs,
            uniformMap : uniformMap
        }).notContextToRender([0, 0, 0, 0]);

        ContextLimits._highpFloatSupported = highpFloatSupported;
    });

    it('1 level function dependency', function() {
        var fs =
            'void main() { \n' +
            '  czm_testFunction1(vec4(1.0)); \n' +
            '}';

        expect({
            context : context,
            fragmentShader : fs
        }).contextToRender();
    });

    it('2 level function dependency', function() {
        var fs =
            'void main() { \n' +
            '  czm_testFunction2(vec4(1.0)); \n' +
            '}';

        expect({
            context : context,
            fragmentShader : fs
        }).contextToRender();
    });

    it('3 level function dependency', function() {
        var fs =
            'void main() { \n' +
            '  czm_testFunction3(vec4(1.0)); \n' +
            '}';

        expect({
            context : context,
            fragmentShader : fs
        }).contextToRender();
    });

    it('diamond dependency', function() {
        var fs =
            'void main() { \n' +
            '  vec4 color = vec4(1.0, 1.0, 1.0, 0.8); \n' +
            '  color = czm_testDiamondDependency1(color); \n' +
            '  color = czm_testDiamondDependency2(color); \n' +
            '  gl_FragColor = color; \n' +
            '}';

        expect({
            context : context,
            fragmentShader : fs
        }).contextToRender();
    });

    it('diamond plus 3 level function dependency', function() {
        var fs =
            'void main() { \n' +
            '  vec4 color = vec4(1.0, 1.0, 1.0, 0.8); \n' +
            '  color = czm_testDiamondDependency1(color); \n' +
            '  color = czm_testDiamondDependency2(color); \n' +
            '  czm_testFunction3(color); \n' +
            '}';

        expect({
            context : context,
            fragmentShader : fs
        }).contextToRender();
    });

    it('big mess of function dependencies', function() {
        var fs =
            'void main() { \n' +
            '  vec4 color = vec4(0.9, 0.9, 1.0, 0.6); \n' +
            '  color = czm_testDiamondDependency1(color); \n' +
            '  color = czm_testDiamondDependency2(color); \n' +
            '  czm_testFunction4(color); \n' +
            '}';

        expect({
            context : context,
            fragmentShader : fs
        }).contextToRender();
    });

    it('doc comment with reference to another function', function() {
        var fs =
            'void main() { \n' +
            '  vec4 color = vec4(1.0, 1.0, 1.0, 1.0); \n' +
            '  czm_testFunctionWithComment(color); \n' +
            '}';

        expect({
            context : context,
            fragmentShader : fs
        }).contextToRender();
    });

    it('compiles with #version at the top', function() {
        var vs =
            '#version 100 \n' +
            'attribute vec4 position; void main() { gl_Position = position; }';
        var fs =
            '#version 100 \n' +
            'void main() { gl_FragColor = vec4(1.0); }';
        sp = ShaderProgram.fromCache({
            context : context,
            vertexShaderSource : vs,
            fragmentShaderSource : fs
        });
    });

    it('compiles with #version after whitespace and comments', function() {
        var vs =
            '// comment before version directive. \n' +
            '#version 100 \n' +
            'attribute vec4 position; void main() { gl_Position = position; }';
        var fs =
            '\n' +
            '#version 100 \n' +
            'void main() { gl_FragColor = vec4(1.0); }';
        sp = ShaderProgram.fromCache({
            context : context,
            vertexShaderSource : vs,
            fragmentShaderSource : fs
        });
    });

    it('fails vertex shader compile', function() {
        if (webglStub) {
            return; // WebGL Stub does not actually try to compile the shader
        }

        var vs = 'does not compile.';
        var fs = 'void main() { gl_FragColor = vec4(1.0); }';
        sp = ShaderProgram.fromCache({
            context : context,
            vertexShaderSource : vs,
            fragmentShaderSource : fs
        });

        expect(function() {
            sp._bind();
        }).toThrowRuntimeError();
    });

    it('fails fragment shader compile', function() {
        if (webglStub) {
            return; // WebGL Stub does not actually try to compile the shader
        }

        var vs = 'void main() { gl_Position = vec4(0.0); }';
        var fs = 'does not compile.';
        sp = ShaderProgram.fromCache({
            context : context,
            vertexShaderSource : vs,
            fragmentShaderSource : fs
        });

        expect(function() {
            sp._bind();
        }).toThrowRuntimeError();
    });

    it('fails to link', function() {
        if (webglStub) {
            return; // WebGL Stub does not actually try to compile and link the shader
        }

        var vs = 'void nomain() { }';
        var fs = 'void nomain() { }';
        sp = ShaderProgram.fromCache({
            context : context,
            vertexShaderSource : vs,
            fragmentShaderSource : fs
        });

        expect(function() {
            sp._bind();
        }).toThrowRuntimeError();
    });

    it('fails with built-in function circular dependency', function() {
        var vs = 'void main() { gl_Position = vec4(0.0); }';
        var fs = 'void main() { czm_circularDependency1(); gl_FragColor = vec4(1.0); }';
        expect(function() {
            sp = ShaderProgram.fromCache({
                context : context,
                vertexShaderSource : vs,
                fragmentShaderSource : fs
            });
            sp._bind();
        }).toThrowDeveloperError();
    });
}, 'WebGL');
